<?php

namespace liketp\route;

use Closure;
use liketp\exception\Exception;
use liketp\Request;
use liketp\Response;
use liketp\Route;
use liketp\route\dispatch\Callback as CallbackDispatch;
use liketp\route\dispatch\Controller as ControllerDispatch;
use liketp\route\dispatch\Url as UrlDispatch;

class RuleItem extends Rule
{
    protected $autoOptions = false;
    protected $isMiss = false;

    public function __construct(Route $router, RuleGroup $parent, ?string $name = null, string $rule = '', $route = null, string $method = '*')
    {
        $this->router = $router;
        $this->parent = $parent;
        $this->name = $name;
        $this->route = $route;
        $this->method = $method;
        $this->setRule($rule);
    }

    public function check(Request $request, string $url, bool $completeMatch = false)
    {
        return $this->checkRule($request, $url, null, $completeMatch);
    }

    public function checkRule(Request $request, string $url, $match = null, bool $completeMatch = false)
    {
        if ($this->checkOption($this->option, $request) == false)  return false;

        $option = $this->getOption();
        $pattern = $this->getPattern();
        $url = $this->urlSuffixCheck($request, $url, $option);

        if (is_null($match)) {
            $match = $this->match($url, $option, $pattern, $completeMatch);
        }

        if ($match === false) return false;

        return $this->parseRule($request, $this->rule, $this->route, $url, $option, $match);
    }

    public function setAutoOptions()
    {
        $this->autoOptions = true;
        return $this;
    }

    public function isAutoOptions()
    {
        return $this->autoOptions;
    }

    public function setMiss()
    {
        $this->isMiss = true;
        return $this;
    }

    public function isMiss()
    {
        return $this->isMiss;
    }


    //------------------------------------------------ protected
    protected function parseRule(Request $request, string $rule, $route, string $url, array $option = [], array $matches = [])
    {
        $vars = array_merge($matches, $this->getAppend());
        $this->vars = $vars;

        if ($prefix = $this->getPrefix()) {
            $route = $prefix . $route;
        }

        if (is_string($route)) {
            $pattern = $replace = [];
            foreach ($matches as $key => $val) {
                $pattern[] = '<' . $key . '>';
                $replace[] = $val;
                $pattern[] = ':' . $key;
                $replace[] = $val;
            }
            $route = str_replace($pattern, $replace, $route);
        }

        return $this->dispatch($request, $route);
    }

    protected function dispatch($request, $route)
    {
        if ($route instanceof Closure) {

            return new CallbackDispatch($request, $this, $route, $this->vars);
        } else if (is_string($route)) {

            if (strpos($route, '@') || strpos($route, '::')) {

                $route = str_replace('/', '\\', $route);
                return $this->dispatchClassMethod($request, $route);
            } else {

                return $this->dispatchControllerAction($request, $route);
            }
        }
    }

    protected function dispatchClassMethod($request, $route)
    {
        if (strpos($route, '@')) {

            $route = explode('@', $route);
            $invokeFn = 'invokeClassMethod';
        } else if (strpos($route, '::')) {

            $route = explode('::', $route);
            $invokeFn = 'invokeStaticMethod';
        }
        $fn = function () use ($invokeFn, $route) {
            return $this->router->app()->{$invokeFn}($route[0], $route[1]);
        };
        return new CallbackDispatch($request, $this, $fn, $this->vars);
    }

    protected function dispatchControllerAction($request, $route)
    {
        $vars = $this->vars;
        [$controller, $action] = explode('/', $route, 2);

        if (substr($controller, 0, 1) == ':') {
            $controller = $vars[substr($controller, 1)] ?? '';
        }
        if (substr($action, 0, 1) == ':') {
            $action = $vars[substr($action, 1)] ?? '';
        }

        if (empty($controller) || empty($action)) {
            throw new Exception('controller or action is empty');
        }

        return new ControllerDispatch($request, $this, [$controller, $action], $vars);
    }

    protected function setRule(string $rule)
    {
        // $ suffix
        if (substr($rule, -1, 1) == '$') {
            $rule = substr($rule, 0, -1);
            $this->completeMatch(true);
        }

        // prepend parent fullName
        $rule = $rule == '/' ? '' : ltrim($rule, '/');
        $prefix = $this->parent->fullName();
        if ($prefix) {
            $rule = $prefix . ($rule ? '/' . $rule : '');
        }

        // process variables
        if (strpos($rule, ':') !== false) {
            $rule = preg_replace(['/\[\:(\w+)\]/', '/\:(\w+)/'], ['<\1?>', '<\1>'], $rule);
        }

        $this->rule = $rule;
    }

    // return 'success' | 'fail' | 'continue'
    protected function checkStaticPartment(string $url, string $rule, bool $completeMatch)
    {
        // static rule
        if (strpos($rule, '<') === false) {
            if ($completeMatch) {
                return $url === $rule ? 'success' : 'fail';
            } else {
                if (0 === strcasecmp($rule . '/', substr($url . '/', 0, strlen($rule . '/')))) {
                    return 'success';
                } else {
                    return 'fail';
                }
            }
        }

        // static partment
        if ($ruleParts = preg_split('/\/<\w+\??>/', $rule, 2)) {
            if ($ruleParts[0] && 0 !== strncasecmp($url, $rule, strlen($ruleParts[0]))) {
                return 'fail';
            }
        }

        return 'continue';
    }

    protected function pickVariables(string $url, string $rule, bool $completeMatch)
    {
        $vars = [];

        $res = preg_match_all('/\/(\<\w+\??\>)/', $rule, $match);
        if ($res === false) return false;
        if ($res === 0) return $vars;

        $replacements = array_map(function ($var) {
            return $this->buildVarExp($var);
        }, $match[1]);
        $regexp = str_replace($match[0], $replacements, $rule);
        $regexp .= $completeMatch ? '$' : '';

        try {
            if (!preg_match('~^' . $regexp . '~', $url, $varMatch)) {
                return false;
            }
        } catch (\Throwable $e) {
            throw new Exception('route pattern match failed');
        }

        foreach ($varMatch as $k => $v) {
            if (is_string($k)) {
                $vars[$k] = $v;
            }
        }

        return $vars;
    }

    protected function buildVarExp(string $variable)
    {
        $required = true;
        if (substr($variable, -2) == '?>') {
            $required = false;
            $variable = substr($variable, 0, -2) . '>';
        }
        $decoration = $required ? '' : '?';

        $pattern = $this->getPattern(substr($variable, 1, -1), '\w+');

        return '(/(?' . $variable . $pattern . '))' . $decoration;
    }

    protected function urlSuffixCheck(Request $request, string $url, array $option = []): string
    {
        if (isset($option['ext'])) {
            $url = str_replace('/\.' . $request->ext() . '$/i', '', $url);
        }
        return $url;
    }


    //------------------------------------------------ private
    private function match(string $url, array $option, array $pattern, bool $completeMatch)
    {
        if (isset($option['complete_match'])) {
            $completeMatch = $option['complete_match'];
        }

        $vars = [];
        $url = $url ? $url : '/';
        $rule = '/' . $this->rule;

        if ($this->getOption('is_miss')) return $vars;
        if ($rule == '/' && $url != '/') return false;

        // res = success | fail | continue
        $res = $this->checkStaticPartment($url, $rule, $completeMatch);
        if ($res == 'success') return $vars;
        if ($res == 'fail') return false;

        // variable match
        $vars = $this->pickVariables($url, $rule, $completeMatch);
        if ($vars === false) return false;

        return $vars;
    }
}
